﻿// Tribe Framework by Chinajade.
//
// LICENSE:
// This work is licensed under the
//     Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
// also known as CC-BY-NC-SA.  To view a copy of this license, visit
//      http://creativecommons.org/licenses/by-nc-sa/3.0/
// or send a letter to
//      Creative Commons // 171 Second Street, Suite 300 // San Francisco, California, 94105, USA.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

using Styx.WoWInternals;
using Styx.WoWInternals.WoWObjects;


namespace Chinajade.Tribe
{
    // Design Notes:
    //
    // * At the time of this writing, the HBcore 'global' blacklist will also prevent looting.
    //   (I.e., it doesn't capture a 'reason' for blacklisting.) We need more than that.
    //   Since the HBcore blacklist is not built to instantiate, we have to roll our own. <sigh>
    //
    // * We capture 'duration' in milliseconds in a 'long'.  This allows almost 3 years:
    //      9,223,372,036,854,775,807 / 1000 MillisecsPerSec / 60 SecsPerMin / 60 MinsPerHour / 24 HoursPerDay
    //          / 365 DaysPerYear = 2.9247 years
    //   Since Windoze itself requires rebooting several times a month (patch days et al), we deem
    //   the internal 'long' representation to be sufficient.

    [RevisionControlId("$Id: TribeBlacklist.cs 11 2012-12-27 08:14:47Z chinajade247@gmail.com $")]
    public class TribeBlacklist
    {
        public const long       DURATIONINMILLISECONDS_DEFAULT  = (15/*mins*/ * 60 * 1000);   // Milliseconds
        public const long       DURATIONINMILLISECONDS_MAX      = (3/*hours*/ * 60 * 60 * 1000);   // Milliseconds

        public enum BlacklistReason
        {
            Harvesting,
            Interacting,
            Killing,
            Looting,
        };


        public TribeBlacklist(long sweepTimePeriodInMilliseconds = DEFAULT_SWEEP_TIME_PERIOD)
        {
            _stopWatchForSweeping.Start();
            _sweepTimePeriodInMilliseconds = sweepTimePeriodInMilliseconds;
            _sweepTimeNextCheck = sweepTimePeriodInMilliseconds;
        }


        /// <summary>
        /// <para>Adds an item identified by GUID to the Blacklist for the specified REASON.
        /// The GUID will be blacklisted for DURATIONINMILLISECONDS.</para>
        /// <para>Notes:<list type="bullet">
        /// <item><description><para>* Each GUID/REASON combination represents a unique entry in the blacklist.
        /// I.e., a single GUID can be blacklisted for different REASONs (and with different expiration times),
        /// and the different entries are treated independently.</para></description></item>
        /// <item><description><para>If the blacklisted GUID/REASON already exists,
        /// the DURATIONINMILLISECONDS is updated to the value provided.</para></description></item>
        /// <item><description><para>* Blacklisting a lot of mobs for long durations can cause the list to grow
        /// large, and may impact performance.</para>
        /// <para>For this reason, we impose an upper limit of DURATIONINMILLISECONDS_MAX on the allowed duration.</para></description></item>
        /// </list></para>
        /// </summary>
        /// <param name="guid"></param>
        /// <param name="reason"></param>
        /// <param name="durationInMilliseconds">must be on the partially-closed interval (0..DURATIONINMILLISECONDS_MAX]</param>
        public void Add(ulong guid,
                        BlacklistReason reason,
                        long durationInMilliseconds = DURATIONINMILLISECONDS_DEFAULT)
        {
            TribeContract.Requires((0 < durationInMilliseconds) && (durationInMilliseconds <= DURATIONINMILLISECONDS_MAX),
                () => string.Format("0 < durationInMilliseconds <= DURATIONINMILLISECONDS_MAX (durationInMilliseconds: {0})",
                    durationInMilliseconds));

            RemoveExpired();

            long expiryTime = checked(_stopWatchForSweeping.ElapsedMilliseconds + durationInMilliseconds);

            _blackList[new BlacklistKey(guid, reason)] = expiryTime;
        }

        /// <summary>
        /// Convenience wrapper around <c>Add(ulong guid, BlacklistReason reason, long durationInMilliseconds)</c>.
        /// </summary>
        /// <param name="guid"></param>
        /// <param name="reason"></param>
        /// <param name="duration"></param>
        public void Add(ulong guid,
                        BlacklistReason reason,
                        TimeSpan duration)
        {
            Add(guid, reason, checked((long)duration.TotalMilliseconds));
        }


        /// <summary>
        /// 
        /// </summary>
        public void DebugDump()
        {
            RemoveExpired();

            foreach (var entry in _blackList.Keys.OrderBy(e => e.Guid))
            {
                string entryAsString = EntryAsString(entry);

                if (entryAsString != null)
                    { Tribe.Log.DeveloperInfo(entryAsString); }
            }
        }


        /// <summary>
        /// <para>Returns true, if blacklist time remains for the GUID blacklisted for REASON.
        /// If REASON, is not specified, then if any entry is associated with the GUID, true is returned.</para>
        /// <para>Notes:<list type="bullet">
        /// <item><description><para>* It is more efficient to specify REASON, if possible.</para></description></item>
        /// </list></para>
        /// </summary>
        /// <param name="guid"></param>
        /// <param name="reason"></param>
        /// <returns></returns>
        public bool IsBlacklisted(ulong guid, BlacklistReason? reason = null)
        {
            // Check for a specific reason...
            if (reason != null)
            {
                long            expiryTime;
                BlacklistKey    key = new BlacklistKey(guid, reason.Value);

                if (_blackList.TryGetValue(key, out expiryTime))
                    { return (expiryTime > _stopWatchForSweeping.ElapsedMilliseconds); }
            }
            
            else // Check if GUID is blacklisted for _any_ reason...
            {
                foreach (BlacklistReason r in Enum.GetValues(typeof(BlacklistReason)))
                {
                    if (IsBlacklisted(guid, r))
                        return (true);
                }
            }

            return (false);
        }


        /// <summary>
        /// <para>Removes the GUID/REASON entry from the blacklist.  If REASON, is not specified,
        /// then all entries associated with the GUID will be removed.</para>
        /// <para>Notes:<list type="bullet">
        /// <item><description><para>* It is more efficient to specify REASON, if possible.</para></description></item>
        /// </list></para>
        /// </summary>
        /// <param name="guid"></param>
        /// <param name="reason"></param>
        private void Remove(ulong guid, BlacklistReason? reason = null)
        {
            // Remove specific entry if reason specified...
            if (reason != null)
                { _blackList.Remove(new BlacklistKey(guid, reason.Value)); }

            else // Remove all entries, if no reason specified...
            {
                foreach (BlacklistReason r in Enum.GetValues(typeof(BlacklistReason)))
                    { _blackList.Remove(new BlacklistKey(guid, r)); }
            }
        }


        private void RemoveExpired()
        {
            if (_stopWatchForSweeping.ElapsedMilliseconds > _sweepTimeNextCheck)
            {
                long                        expiryTime  = _stopWatchForSweeping.ElapsedMilliseconds;
                IEnumerable<BlacklistKey>   expiredKeys = (_blackList
                                                            .Where(kvp => kvp.Value <= expiryTime)
                                                            .Select(kvp => kvp.Key)
                                                            .ToList());

                foreach (BlacklistKey key in expiredKeys)
                    { _blackList.Remove(key); }

                _sweepTimeNextCheck = _stopWatchForSweeping.ElapsedMilliseconds + _sweepTimePeriodInMilliseconds;
            }
        }

        private struct BlacklistKey
        {
            public BlacklistKey(ulong guid, BlacklistReason reason)
                : this()
            {
                Guid = guid;
                Reason = reason;
            }

            public ulong Guid { get; private set; }
            public BlacklistReason Reason { get; private set; }
        }

        private string EntryAsString(BlacklistKey entry)
        {
            long currentTime = _stopWatchForSweeping.ElapsedMilliseconds;
            long expiryTime;

            if (_blackList.TryGetValue(entry, out expiryTime) && (expiryTime > currentTime))
            {
                WoWObject wowObject = ObjectManager.GetObjectByGuid<WoWObject>(entry.Guid);

                return (string.Format("Blacklist({0}, {1}, {2})",
                    ((wowObject != null)
                        ? wowObject.LoggableIdentifier()
                        : string.Format("x{0}", entry.Guid.AsGuidHash())),
                    entry.Reason,
                    (expiryTime - currentTime).AsHumanTime()
                    ));
            }

            return (null);
        }

        private const long      DEFAULT_SWEEP_TIME_PERIOD = (3 * 60 * 1000);  // Milliseconds

        private Dictionary<BlacklistKey, long>      _blackList = new Dictionary<BlacklistKey, long>();
        private Stopwatch                           _stopWatchForSweeping = new Stopwatch();
        private long                                _sweepTimeNextCheck;
        private readonly long                       _sweepTimePeriodInMilliseconds;
    }
}
